<?php
// -------------------------------------------
// WebSocket服务器
// 消息类型：t:文本 i:图片 m:提示信息 l:消息记录 b:撤回消息
// c 代表内容
// -------------------------------------------

// 创建websocket服务器对象，监听0.0.0.0:9502端口
$ws = new swoole_websocket_server("0.0.0.0", 9502);

// 监听WebSocket连接打开事件
$ws->on('open', function ($ws, $request) {
    # 将监听的用户存储到redis缓存中
    $fd = $request->fd;
    # 获取消息记录
    $message_log = getMessageLog();
    # 将消息记录推送给当前登录的用户
    pushForOne($ws,$fd,['type' => 'l','log'=>$message_log]);
});

// 监听WebSocket消息事件
$ws->on('message', function ($ws, $request) {
	# 接收消息
	$msg = json_decode($request->data,true);
	if(!is_array($msg) && array_key_exists('type', $msg)) return;
	$type = $msg['type'];
	if(!in_array($type, ['t','i','u','c']) || empty($msg[$type])) return;
	$uid  = $msg['uid'];
	if(empty($uid) || !is_numeric($uid)) return;
	# 判断消息类型
	if($type == 't' || $type == 'i'){
		# 如果是文本消息或者图片消息,分别推送给其他人和自己
		$msg['from'] = getUserInfo($uid);
		# 生成专属token，用于识别对应消息
		$msg['token'] = createToken($request->fd,$uid);
		# 存储消息记录，最多存储20条记录
		intoQueue($msg);
		if(countQueueLen() > 20){
			# 最后被删除的一条数据，返回值为数组
			$res = outQueue(true);
		}

		$msg['me'] = 1;
		pushForOne($ws,$request->fd,$msg);
		# 推送给其他人
		$msg['me'] = 0;
		pushForOther($ws,$request->fd,$msg);
	}elseif ($type == 'u') {
		# 存储用户信息
		setClientId($request->fd,json_encode(['time'=>time(),'uid'=>$msg['uid']]));
		# 欢迎信息
		$text = '欢迎用户'.$msg['uid'].'加入群聊';
		# 将欢迎消息推送给所有人
   		pushForAll($ws, ['type' => 'm','c'=>$text,'count'=>countClientNum()]);
	}elseif ($type == 'c') {
		# 撤回消息
		$text = '用户'.$msg['uid'].'撤回了一条消息';
		pushForAll($ws, ['type' => 'b','b'=>$msg['c'],'c'=>$text]);
	}
});

// 监听WebSocket连接关闭事件
$ws->on('close', function ($ws, $fd) {
	# 移除redis中的用户
	delClientId($fd);
	$text = '一位用户已退出群聊';
	pushForAll($ws, ['type' => 'm','c'=>$text,'count'=>countClientNum()]);
});

$ws->start();


// ------------------------------------------------
//  用户信息
// ------------------------------------------------
function getUserInfo($uid)
{
	# 模拟用户数据
	return $uid;
}

// 统计在线客户端人数
function countClientNum()
{
	return RedisObj()->hLen('client_person');
}

// 为每条消息生成专属token
function createToken($fd,$uid)
{
	return $fd.getNoncestr(12).$uid;
}

// ------------------------------------------------
//  消息推送：单个，群发，除指定之外的用户
// ------------------------------------------------

// 向单个用户发送消息
function pushForOne($ws, $fd, $msg, $str = false)
{
	# 如果字符串有值则使用该字符串
	$str || $msg = json_encode($msg);
	return $ws->push($fd,$msg);
}

// 向所有客户端发送消息
function pushForAll($ws, $msg, $str = false)
{
	$client_person = getAllClientId();
	foreach ($client_person as $v) {
		pushForOne($ws, $v, $msg, $str);
	}
}

// 向除了指定客户端之外的其他客户端发送消息
function pushForOther($ws, $fd, $msg, $str = false)
{
	$client_person = getAllClientId();
	foreach ($client_person as $v) {
		if($fd != $v){
			pushForOne($ws, $v, $msg, $str);
		}
	}
}

// ------------------------------------------------
//  客户端ID：设置、删、获取时间、获取全部ID
// ------------------------------------------------

// 向名为 client_person 的hash中添加元素 $fd->$time
function setClientId($fd,$arr)
{
	return RedisObj()->hSet('client_person',$fd,$arr);
}

// 获取全部客户端
function getAllClientId()
{
	return RedisObj()->hKeys('client_person');
}

// 删除某个客户端
function delClientId($fd)
{
	return RedisObj()->hDel('client_person',$fd);
}

// 获取某个客户端的时间
function getClientTime($fd)
{
	return RedisObj()->hGet('client_person',$fd);
}

// ------------------------------------------------
//  消息记录队列
// ------------------------------------------------

// 进队
function intoQueue($msg)
{
	return RedisObj()->rPush('message_log',json_encode($msg));
}

// 出队,将队列左边的最后一个元素删除
function outQueue($isArr = false)
{
	return json_decode(RedisObj()->lPop('message_log'),$isArr);
}

// 队列长度
function countQueueLen()
{
	return RedisObj()->lLen('message_log');
}

// 获取队列中的所有消息记录
function getMessageLog()
{
	return RedisObj()->lRange('message_log',0,-1);
}

// 生成随机码
function getNoncestr($num=8)
{
    $arr = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f','g','h','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','1','2','3','4','5','6','7','8','9','0','_','?',']','[','*','#'];
    $tmpstr = '';
    $max = count($arr) - 1;
    for($i = 1;$i <= $num;$i++){
        $key = rand(1,$max);
        $tmpstr .= $arr[$key];
    }

    return $tmpstr;
}

// ------------------------------------------------
//  pdo语句
// ------------------------------------------------
function pdoSql($sql, $bind = [], $exec = false)
{
    try {
        $pdo = new PDO("mysql:host=127.0.0.1;dbname=oursql", 'xml', 'MM:xml310');
        $pdo->query('SET NAMES utf8');
    } catch (\PDOException $e) {
        file_put_contents('sql_log', date('Y-m-d H:i:s') . '=' . iconv("GB2312//IGNORE", "UTF-8", $e->getMessage()), FIEL_APPEND);
    }
    $bind = empty($bind) ? false : $bind;
    if ($bind) {
        $sth = $pdo->prepare($sql);
        $sth->execute($bind);
    } else {
        $sth = $exec ? $pdo->exec($sql) : $pdo->query($sql);
    }
    if ($exec) {
        $pdo = null;
        return $sth;
    } else {
        $sth->setFetchMode(PDO::FETCH_ASSOC);
        $arr = [];
        while ($v = $sth->fetch()) {
            $arr[] = $v;
        }
        $sth = null;
        return $arr;
    }
}

// -------------------------------------------
// Redis对象
// -------------------------------------------
function RedisObj()
{
	static $redis = null;
	if(is_null($redis)){
		$redis = new Redis();
		$redis->connect('127.0.0.1',6379);
		$redis->auth('yqy1994');
	}
	return $redis;
}